package com.googlecode.lightest.core

import org.apache.commons.cli.GnuParser
import org.codehaus.groovy.runtime.StackTraceUtils 
import org.testng.TestNG
import org.testng.xml.XmlSuite

class TestRunner implements ITestRunner {
    InputStream _in
    PrintStream out
    GroovyClassLoader classLoader
    TestNG testng
    LightestTestListener reporter
    List<ITaskDispatcher> dispatchers
    InteractiveTaskDispatchStrategy dispatchStrategy
    boolean interactiveThreadShouldStop

    TestRunner() {
        this(System.in, System.out)
    }
    
    TestRunner(InputStream _in, PrintStream out) {
        this._in = _in
        this.out = out
        
        classLoader = new GroovyClassLoader(this.class.classLoader)
        testng = new TestNG()
        testng.setUseDefaultListeners(false)
        dispatchStrategy = new InteractiveTaskDispatchStrategy(_in, out)
    }

    /**
     * Returns the output directory
     */
    File getOutputDir() {
        return new File(testng.getOutputDirectory())
    }

    /**
     * Configures the runner by wiring together dependencies as specified by
     * the configuration file.
     *
     * @param configFile
     */
    void configure(File configFile) {
        configure(configFile.text)
    }

    /**
     * Configures the runner by wiring together dependencies as specified by
     * the configuration text.
     *
     * @param configText
     */
    void configure(String configText) {
        IConfiguration config = new Configuration()
        config.init(configText)
        configure(config)
    }

    void configure(IConfiguration config = new Configuration()) {
        def classPaths = config.getClassPaths()
        def prefs = config.getPreferences()
        def envs = config.getEnvironments()
        def strategy = config.getDispatcherAssignmentStrategy()
        
        reporter = config.getReporter()
        dispatchers = []

        envs.each { env ->
            def dispatcher = new TaskDispatcher()
            dispatcher.setPreferences(prefs)
            dispatcher.setEnvironment(env)
            dispatcher.setStrategy(dispatchStrategy)
            dispatcher.setListener(reporter)

            dispatchers << dispatcher
        }

        strategy.setDispatchers(dispatchers)
        reporter.setDispatcherAssignmentStrategy(strategy)
        reporter.setOutputDir(config.getOutputDir())
        testng.addListener((Object) reporter)
        testng.setOutputDirectory(config.getOutputDir())

        classLoader = config.getClassLoader()

        classPaths.each { path ->
            def url = new File(path).getCanonicalFile().toURL()
            classLoader.addURL(url)
        }
    }

    /**
     * Sets the output directory
     *
     * @param outputDirectory
     */
    void setOutputDir(File dir) {
        def outputDir = dir.getCanonicalPath()

        testng.setOutputDirectory(outputDir)
        
        if (reporter instanceof ILightestReporter) {
            reporter.setOutputDir(outputDir)
        }
    }

    /**
     * Sets whether to refresh the report before each test is run. If set to
     * true, the report will be created in the directory that getOutputDir()
     * returns. Refreshing the report slows down test execution somewhat. For
     * longer running tests this will not be noticeable, while it may bog down
     * fast tests.
     *
     * @param refreshReport
     */
    void setRefreshReport(boolean refreshReport) {
        if (reporter instanceof ILightestReporter) {
            reporter.setRefreshReport(refreshReport)
        }
    }

    /**
     * Sets whether to generate the HTML report, in addition to the standard
     * TestNG XML report. Large reports may take a while to generate.
     *
     * @param refreshReport
     */
    void setGenerateHTML(boolean generateHTML) {
         if (reporter instanceof ILightestReporter) {
             reporter.setGenerateHTML(generateHTML)
         }
    }
    
    /**
     * Uses a given class loader to load a Groovy class from a file.
     *
     * @param testFile
     * @param classLoader
     */
    private Class getClassFromFile(File testFile, GroovyClassLoader gcl) {
        assert testFile.exists()
        return classLoader.parseClass(testFile.text)
    }
    
    void setSuites(List<String> paths) {
        def suitePaths = []
        def testClasses = []

        paths.each { path ->
            if (path.endsWith('.xml')) {
                suitePaths << path
            }
            else if (path.endsWith('.groovy')) {
                testClasses << getClassFromFile(new File(path), classLoader)
            }
            else {
                out.println "Skipping unrecognized file: ${path}"
            }
        }

        if (testClasses) {
            suitePaths << LightestUtils.createSuiteFile(testClasses).getPath()
        }

        testng.setTestSuites(suitePaths)
    }

    /**
     * Executes the test suites represented by XML files, along with tests
     * represented as Groovy files, whose paths are provided as a parameter to
     * this method. The contextual class loader of the TestNG run is a
     * GroovyClassLoader that is aware of any task classpaths specified in the
     * Configuration.
     *
     * @param paths  paths to files that are either Lightest suite XML files,
     *               or Groovy scripts that are LightestTestCase's.
     */
    void run(List<String> paths) {
        setSuites(paths)
        run()
    }
    
    void run() {
        startInteractiveThread()
        Thread.currentThread().setContextClassLoader(classLoader)
        testng.run()
        stopInteractiveThread()
    }
    
    /**
     * Starts a thread that reads the runner's input stream for a directive to
     * start interactive mode. This is typically whenever the user hits the
     * Enter/Return key. The content of the line entered will be discarded.
     * 
     * @param is  the input stream to receive information from 
     */
    protected void startInteractiveThread() {
        def interactiveThread = [
            run: {
                def reader = _in.newReader()
            
                while (! interactiveThreadShouldStop) {
                    if (reader.ready()) {
                        reader.readLine()
                        interrupt()
                    }
                    
                    sleep 100
                }
            }
        ] as Thread
        
        interactiveThreadShouldStop = false
        interactiveThread.start()
    }
    
    /**
     * Instructs the input scanning thread to stop at the next convenient time.
     * If the thread is not currently running, this is a no-op.
     */
    protected void stopInteractiveThread() {
        interactiveThreadShouldStop = true
    }
    
    /**
     * Prepares to enter interactive mode in response to user input.
     * Interactive mode cannot be entered when tests are being run in parallel.
     * We can only know whether tests are being run in parallel after the
     * TestNG suites have been set, e.g. after setSuites() has been called.
     * This method will fail with an assertion error if called beforehand.
     * 
     * @param interactive
     */
    protected void interrupt() {
        assert testng.m_suites.size() > 0
        
        def parallel = testng.m_suites.any {
            it.getParallel() == XmlSuite.PARALLEL_TESTS ||
            it.getParallel() == XmlSuite.PARALLEL_METHODS
        }
        
        if (! parallel) {
            dispatchStrategy.interrupt()
        }
        else {
            out.println ('[running tests in parallel - cannot interrupt]')
        }
    }
    
    static void main(args) {
        final USAGE = ('java -jar lightest-core-standalone.jar '
            + '[OPTIONS] CONFIGFILE [TESTFILE | SUITEFILE]...')
        
        def cli = new CliBuilder(usage: USAGE, parser: new GnuParser())
        
        cli.i(longOpt: 'interactive', required: false,
            'start in interactive mode')
        
        def options = cli.parse(args)
        def argsList = options.getArgs().toList()
        
        if (argsList.size() < 2) {
            cli.usage()
            return
        }
        
        def testRunner = new TestRunner()

        testRunner.configure(new File(argsList.remove(0)))
        testRunner.setSuites(argsList)
        
        if (options.i) {
            testRunner.interrupt()
        }
        
        try {
            testRunner.run()
        }
        catch (e) {
            StackTraceUtils.deepSanitize(e)
            e.printStackTrace()
            System.exit(1)
        }
    }
}
